TI_GDT equ  0
RPL0  equ   0
SELECTOR_VIDEO equ (3 << 3) + TI_GDT + RPL0  ; 视频段描述符

[SECTION .data]
print_int_buffer:
    dd 0, 0

[SECTION .text]
[BITS 32]
; --------------------------------------------------------
; 打印字符，将栈中的第一个字符打印在光标处
; ebp+8
global print_char
print_char:
    push ebp
    mov ebp, esp
    push eax
    push ebx
    push ecx
    push edx

    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov dx, 0x3cc
    in al, dx                                 ; 仅为看Miscellaneous Output Register值为0x67, 其I/OAS位值为1

    mov ecx, [ebp+8]                          ; 第一个参数
    cmp cl, 0x0D
    je .is_carriage_return
    cmp cl, 0x0A
    je .is_line_feed

    call get_cursor_location
    mov bx, ax
    shl bx, 1                                 ; 左移一位，相当与乘以2，一个字符占两个字节，光标的位置是字符的坐标，所以乘以2
    mov [gs:bx], cl

    add bx, 2                                 ; 光标应该在最后一个字符的后一个位置
    shr bx, 1                                 ; 右移一位，相当与除以2，一个字符占两个字节，光标的位置是字符的坐标，所以除以2
    call set_cursor_location
    jmp .roll_screen

.is_line_feed:                                ; 换行
.is_carriage_return:                          ; 回车，就是把光标移到下一行的行首就行了
    call get_cursor_location
    mov bx, ax
    xor dx, dx
    mov cx, 80                                ; 每行80个字符
    div cx                                    ; 当前坐标除以80
    sub bx, dx                                ; 当前坐标减去刚刚除法的余数，就是当前行的行首
    add bx, 80                                ; 再加上80就是下一行的行首坐标了
    call set_cursor_location
    jmp .roll_screen

.roll_screen:
    call get_screen_start_location            ; 获取当前屏幕的起始位置
    sub bx, ax
    cmp bx, 2000                              ; 如果光标的位置-屏幕的起始位置（bx-ax）不等于2000就结束
    jne .to_ret

    mov bx, ax                                ; 等于2000就让屏幕向上滚动一行
    add bx, 80
    call set_screen_start_location

.to_ret:
    pop edx
    pop ecx
    pop ebx
    pop eax
    pop ebp
    ret

; --------------------------------------------------------
; 打印字符串，栈中的第一个参数是字符串的起始地址，字符串会以0结尾
; ebp+8
global print_str
print_str:
    push ebp
    mov ebp, esp
    push eax
    push ebx
    push ecx
    push edx

    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov edx, [ebp+8]                          ; 第一个参数是字符串的起始地址

    call get_cursor_location
    mov bx, ax
    shl bx, 1                                 ; 左移一位，相当与乘以2，一个字符占两个字节，光标的位置是字符的坐标，所以乘以2
.loop:
    mov cl, [edx]
    cmp cl, 0                                 ; 是0就结束，去处理光标和滚屏
    je .deal_cursor_and_screen
    cmp cl, 0x0D
    je .is_carriage_return
    cmp cl, 0x0A
    je .is_line_feed

    mov [gs:bx], cl

    add bx, 2                                 ; 下一个字符的地址
    inc edx
    jmp .loop

.is_line_feed:                                ; 换行
.is_carriage_return:                          ; 回车，就是把光标移到下一行的行首就行了
    push edx

    xor edx, edx
    mov ax, bx
    mov cx, 80*2                              ; 每行80个字符，一个字符两个字节
    div cx                                    ; 当前坐标除以 80×2

    sub bx, dx                                ; 当前坐标减去刚刚除法的余数，就是当前行的行首
    add bx, 80*2                              ; 再加上 80×2 就是下一行的行首坐标了
    pop edx
    inc edx
    jmp .loop

.deal_cursor_and_screen:
    shr bx, 1                                 ; 右移一位，相当与除以2，一个字符占两个字节，光标的位置是字符的坐标，所以除以2
    call set_cursor_location

.roll_screen:
    push ebx                                  ; 字符串打印完可能上卷不止一行，故是一个loop，暂存光标的位置
    call get_screen_start_location            ; 获取当前屏幕的起始位置
    sub bx, ax
    cmp bx, 2000                              ; 如果光标的位置-屏幕的起始位置（bx-ax）不等于2000就结束
    jbe .to_ret                               ; 不大于就结束

    mov bx, ax
    add bx, 80
    call set_screen_start_location
    pop ebx
    jmp .roll_screen

.to_ret:
    pop ebx                                   ; roll_screen循环退出时后面的代码不会执行，故要在这里pop一下

    pop edx
    pop ecx
    pop ebx
    pop eax
    pop ebp
    ret

; --------------------------------------------------------
; 打印数字，以十进制形式输出，栈中的第一个参数是就是数字，4字节
; ebp+8
global print_int
print_int:
    push ebp
    mov ebp, esp

    push eax
    push ebx
    push ecx
    push edx
    push edi
    push esi

    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov ebx, [ebp+8]                          ; 第一个参数是要打印的数字
    mov ax, bx                                ; 低16位
    mov cl, 16
    shr ebx, cl
    mov dx, bx                                ; 高16位
    mov cx, 10                                ; 除数
    mov esi, 0                                ; 计数器
    mov ebx, print_int_buffer
.loop:
    call divdw

    add cx, 0x30
    mov [ebx + esi], cl                       ; 先放到缓冲中，然后再打印，如果直接打印数字会是反着的
    inc si
    mov cx, 10
    cmp ax, 0
    jne .loop
    cmp dx, 0
    jne .loop                                 ; 商的高16位和低16位都不为0时，才会去打印

    xor eax, eax
    call get_cursor_location
    shl eax, 1
    mov ecx, esi                              ; 循环次数
.print:
    mov dl, [ebx + esi - 1]                   ; 除法循环结束后，esi为循环次数，故-1才为地一个字符
    mov [gs:eax], dl
    add eax, 2
    dec esi                                   ; 下一个字符
    loop .print

    pop esi
    pop edi
    pop edx
    pop ecx
    pop ebx
    pop eax
    pop ebp
    ret

; --------------------------------------------------------
; 解决除法溢出的问题，思想：X/N = int(H/N)*65536 +[rem(H/N)*65536+L]/N
; ax: 被除数低16位
; dx: 被除数高16位
; cx: 除数
; return
; ax: 商的低16位
; dx: 商的高16位
; cx: 余数
divdw:
    push si
    push di
	mov si, ax			                      ; 暂存低16位

	mov ax, dx			                      ; 先算X的高16位的除法
	mov dx, 0
	div cx			                          ; 商在ax中，余数在dx中

	mov di, ax		                          ; 把商暂存起来

	mov ax, si		                          ; 将X低16位放到ax中，因为做X的高16位除法时，余数已经在dx中，相当于左移16位了，此时DX<<16+ax相当于rem(H/N)*65536+L
	div cx				                      ; [rem(H/N)*65536+L]/N，商存在ax中、余数在dx中

	mov cx, dx			                      ; 余数
	mov dx, di			                      ; 高16位

	pop di
	pop si
	ret

; --------------------------------------------------------
; 获取光标位置，光标位置为16位
; return ax
get_cursor_location:
    push dx

    mov dx, 0x3D4
    mov al, 0x0E
    out dx, al
    mov dx, 0x3D5
    in al, dx

    mov ah, al                                ; 获取高8位

    mov dx, 0x3D4
    mov al, 0x0F
    out dx, al
    mov dx, 0x3D5
    in al, dx                                 ; 获取低8位

    pop dx
    ret

; --------------------------------------------------------
; 设置光标位置
; bx 打印的最后的一个字符的地址
set_cursor_location:
    push dx

    mov dx, 0x3D4
    mov al, 0x0E
    out dx, al
    mov dx, 0x3D5
    mov al, bh                                ; 设置高8位
    out dx, al

    mov dx, 0x3D4
    mov al, 0x0F
    out dx, al
    mov dx, 0x3D5
    mov al, bl                                ; 设置低8位
    out dx, al

    pop dx
    ret

; --------------------------------------------------------
; 获取屏幕在显存中的起始位置 16位
; return ax , 这个和光标位置一样，也是按照字符来算的，比如 80 就是第二行了
get_screen_start_location:
    push dx

    mov dx, 0x3D4
    mov al, 0x0C
    out dx, al
    mov dx, 0x3D5
    in al, dx

    mov ah, al                                ; 获取高8位

    mov dx, 0x3D4
    mov al, 0x0D
    out dx, al
    mov dx, 0x3D5
    in al, dx                                 ; 获取低8位

    pop dx
    ret

; --------------------------------------------------------
; 设置屏幕在显存中的起始位置 16位
; bx 屏幕的起始位置
set_screen_start_location:
    push dx

    mov dx, 0x3D4
    mov al, 0x0C
    out dx, al
    mov dx, 0x3D5
    mov al, bh                                ; 设置高8位
    out dx, al

    mov dx, 0x3D4
    mov al, 0x0D
    out dx, al
    mov dx, 0x3D5
    mov al, bl                                ; 设置低8位
    out dx, al

    pop dx
    ret